import type { TimeLineEntry, TimeLineSeries } from './types'
import { dimension, min5List, style } from './util'
import UIItem from './ui-item'
import { drawLine, fillRoundRect, getPosY, strokeRoundRect } from './render-util'

interface Rect {
  x: number
  y: number
  w: number
  h: number
}

// interface Size {
//   w: number
//   h: number
// }

class TimeScale extends UIItem {
  public title = ''
  private t0 = 0
  private t1 = 300 * 10

  public setTimeRange(t0: number, t1: number) {
    this.t0 = t0
    this.t1 = t1
  }

  protected onDraw(ctx: CanvasRenderingContext2D): void {
    const t0 = this.t0
    const t1 = this.t1
    if (t1 <= t0) {
      return
    }
    const w = this.width
    const h = this.height

    ctx.fillStyle = style.timeScaleBgColor
    ctx.fillRect(0, 0, w, h)

    ctx.strokeStyle = style.timeScaleColor
    ctx.lineWidth = 0.3
    drawLine(ctx, 0, 0, 0, h)
    drawLine(ctx, w, 0, w, h)
    const m0 = Math.floor(t0 / 60.0)
    const m1 = Math.floor(t1 / 60.0) + 1
    let m = m0
    while (m < m1) {
      const mY = getPosY(m * 60, t0, t1, 0, h)
      let mX = 0
      let mW: number
      if (m % 5 === 0) {
        mW = dimension.timeScaleMarkLength1
      } else {
        mW = dimension.timeScaleMarkLength0
      }
      drawLine(ctx, mX, mY, mX + mW, mY)
      mX = w - mW
      drawLine(ctx, mX, mY, mX + mW, mY)
      m++
    }

    const fs = dimension.fontSize
    ctx.fillStyle = style.timeScaleColor
    ctx.font = fs + 'px Arial'
    ctx.textAlign = 'center'
    const mX = w * 0.5

    m = Math.floor(m0 / 5) * 5
    if (m < m0) {
      m += 5
    }
    while (m < m1) {
      const s = min5List[m / 5]
      const mY = getPosY(m * 60, t0, t1, 0, h) + fs * 0.5 - 2
      ctx.fillText(s, mX, mY)
      m += 5
    }

    if (this.title !== '') {
      ctx.fillStyle = style.textColor
      ctx.font = '20px Consolas'
      ctx.fillText(this.title, mX, h - 2)
    }
  }
}

class EntryList extends UIItem {
  private t0 = 0
  private t1 = 0

  private right: boolean
  private entryIndex: TimeLineEntry[][] = []
  private readonly entryIndexStep = 3600

  constructor(right: boolean) {
    super()
    this.right = right
  }

  public setRight(ok: boolean) {
    this.right = ok
  }

  public setData(lst: TimeLineEntry[]) {
    const step = this.entryIndexStep
    let size = (48 * 3600) / step
    if ((48 * 3600) % step !== 0) {
      size++
    }
    this.entryIndex = new Array(size)
    let a: TimeLineEntry[] | undefined
    let h = -1
    for (const e of lst) {
      const eH = Math.floor(e.time / step) * step
      if (eH !== h) {
        a = this.entryIndex[eH]
        if (!a) {
          a = []
          this.entryIndex[eH] = a
        }
        h = eH
      }
      a!.push(e)
    }
  }

  public setTimeRange(t0: number, t1: number) {
    this.t0 = t0
    this.t1 = t1
  }

  public onDraw(ctx: CanvasRenderingContext2D) {
    if (this.right) {
      this.onDrawRight(ctx, this.t0, this.t1)
    } else {
      this.onDrawLeft(ctx, this.t0, this.t1)
    }
  }

  private onDrawLeft(ctx: CanvasRenderingContext2D, t0: number, t1: number) {
    const w = this.width
    const h = this.height
    const eX = w - 30
    const eW = w - 30
    const lx0 = w
    const lx1 = eX
    const fs = dimension.fontSize
    const fontXFix = fs * 0.5 - 2
    const lineHight = fs - 2
    ctx.fillStyle = style.textColor
    ctx.strokeStyle = style.textColor
    ctx.lineWidth = 0.3
    ctx.font = fs + 'px Consolas'
    ctx.textAlign = 'right'
    let lastY = h
    this.forEachEntry(t0, t1, (e) => {
      let eY = getPosY(e.time, t0, t1, 0, h)
      const ly0 = eY
      if (eY > lastY - lineHight) {
        eY = lastY - lineHight
      }
      lastY = eY
      if (eY < 0) {
        return false
      }
      ctx.fillText(e.content, eX, eY + fontXFix, eW)
      const ly1 = eY
      drawLine(ctx, lx0, ly0, lx1, ly1)
      return true
    })
  }

  private onDrawRight(ctx: CanvasRenderingContext2D, t0: number, t1: number) {
    const w = this.width
    const h = this.height
    const eX = 30
    const eW = w - 30
    const lx0 = 0
    const lx1 = eX
    const fs = dimension.fontSize
    const fontXFix = fs * 0.5 - 2
    const lineHight = fs - 2
    ctx.fillStyle = style.textColor
    ctx.strokeStyle = style.textColor
    ctx.lineWidth = 0.3
    ctx.font = fs + 'px Consolas'
    ctx.textAlign = 'left'
    let lastY = h
    this.forEachEntry(t0, t1, (e) => {
      let eY = getPosY(e.time, t0, t1, 0, h)
      const ly0 = eY
      if (eY > lastY - lineHight) {
        eY = lastY - lineHight
      }
      lastY = eY
      if (eY < 0) {
        return false
      }
      ctx.fillText(e.content, eX, eY + fontXFix, eW)
      // const ly0 = getPosY(e.time, t0, t1, 0, h)
      const ly1 = eY
      drawLine(ctx, lx0, ly0, lx1, ly1)
      return true
    })
  }

  private forEachEntry(t0: number, t1: number, fn: (e: TimeLineEntry) => boolean) {
    const step = this.entryIndexStep
    const h0 = Math.floor(t0 / step) * step
    const h1 = Math.ceil(t1 / step) * step
    const idx = this.entryIndex

    for (let h = h0; h < h1; h++) {
      const lst = idx[h]
      if (!lst) {
        continue
      }
      for (const e of lst) {
        if (e.time < t0 || e.time >= t1) {
          continue
        }
        if (!fn(e)) {
          return
        }
      }
    }
  }
}

class TimeLine extends UIItem {
  private timeScale = new TimeScale()
  private leftEntryList: EntryList = new EntryList(false)
  private rightEntryList: EntryList = new EntryList(true)

  public setTimeRange(t0: number, t1: number) {
    this.timeScale.setTimeRange(t0, t1)
    this.leftEntryList.setTimeRange(t0, t1)
    this.rightEntryList.setTimeRange(t0, t1)
  }

  public setData(s: TimeLineSeries) {
    this.timeScale.title = s.title
    this.leftEntryList.setData(s.left)
    this.rightEntryList.setData(s.right)
  }

  public setGeometry(x: number, y: number, w: number, h: number) {
    super.setGeometry(x, y, w, h)
    let w1 = dimension.timeScaleWidth
    let x1 = Math.floor((w - w1) * 0.5)
    this.timeScale.setGeometry(x1, 0, w1, h)
    w1 = dimension.entryListWidth
    x1 -= w1
    this.leftEntryList.setGeometry(x1, y, w1, h)
    x1 = x1 + w1 + dimension.timeScaleWidth
    this.rightEntryList.setGeometry(x1, y, w1, h)
  }

  protected onDraw(ctx: CanvasRenderingContext2D): void {
    this.timeScale.draw(ctx)
    this.leftEntryList.draw(ctx)
    this.rightEntryList.draw(ctx)
  }
}

class TimeLineItem extends UIItem {
  protected t0 = 0
  protected t1 = 3600

  public setTimeRange(t0: number, t1: number) {
    this.t0 = t0
    this.t1 = t1
  }

  protected onDraw(ctx: CanvasRenderingContext2D): void {
    throw new Error('Method not implemented.')
  }

  protected getY(t: number) {}
}

class TimeLineList extends UIItem {
  public contentBox: Rect = { x: 0, y: 0, w: 0, h: 0 }
  private timeLines: TimeLine[] = []

  public get scroll(): boolean {
    return this.timeLines.length * dimension.timeLineWidth() > this.width
  }

  public get hPosition(): number {
    return -this.contentBox.x
  }

  public setData(lst: TimeLineSeries[]) {
    if (lst.length > this.timeLines.length) {
      for (let i = this.timeLines.length; i < lst.length; i++) {
        this.timeLines.push(new TimeLine())
      }
    } else if (lst.length < this.timeLines.length) {
      this.timeLines.splice(lst.length)
    }
    for (let i = 0; i < lst.length; i++) {
      this.timeLines[i].setData(lst[i])
    }
    this.setHPos(0)
  }

  public setGeometry(x: number, y: number, w: number, h: number) {
    super.setGeometry(x, y, w, h)
    this.contentBox.h = h
    this.setHPos(0)
  }

  public setHPos(p: number) {
    this.contentBox.h = this.height
    this.contentBox.w = this.timeLines.length * dimension.timeLineWidth()
    const diff = this.contentBox.w - this.width
    if (diff < 0) {
      p = diff / 2.0
    } else {
      if (p < 0) {
        p = 0
      }
      if (p > diff) {
        p = diff
      }
    }
    this.contentBox.x = -p
    let x = this.contentBox.x
    const w = dimension.timeLineWidth()
    this.timeLines.forEach((e) => {
      e.setGeometry(x, 0, w, this.height)
      x += w
    })
  }

  public setTimeRange(t0: number, t1: number) {
    this.timeLines.forEach((e) => e.setTimeRange(t0, t1))
  }

  protected onDraw(ctx: CanvasRenderingContext2D): void {
    this.drawBg(ctx, style.bgColor)
    this.timeLines.forEach((e) => e.draw(ctx))
  }
}

class ScrollBar extends UIItem {
  public pos = 0.1
  public len = 0.05
  private horizon = false
  private sliderBox: Rect = { x: 0, y: 0, w: 0, h: 0 }

  public get position(): number {
    return this.pos
  }

  public get sliderRange(): number {
    if (this.horizon) {
      return this.width - 10
    }
    return this.height - 10
  }

  constructor(horizon: boolean) {
    super()
    this.horizon = horizon
  }

  public setGeometry(x: number, y: number, w: number, h: number) {
    super.setGeometry(x, y, w, h)
    this.updateSlierBox()
  }

  public insideSlier(x: number, y: number) {
    const b = this.sliderBox
    const bx = this.left + b.x
    const by = this.top + b.y
    return x >= bx && x <= bx + b.w && y >= by && y <= by + b.h
  }

  public setSliderGeometryX(l0: number, l1: number, s0: number, s1: number) {
    const l = l1 - l0
    if (l === 0.0) {
      this.setSliderGeometry(0, 0)
    } else {
      this.setSliderGeometry(s0 / l, (s1 - s0) / l)
    }
  }

  public setSliderGeometry(pos: number, len: number) {
    if (len < 0.0) {
      len = 0.0
    }
    if (len >= 1.0) {
      len = 1.0
    }
    if (pos < 0.0) {
      pos = 0.0
    }
    if (pos > 1.0 - len) {
      pos = 1.0 - len
    }
    this.pos = pos
    this.len = len
    this.updateSlierBox()
  }

  protected onDraw(ctx: CanvasRenderingContext2D): void {
    this.drawBg(ctx, style.bgColor)
    ctx.fillStyle = style.scrollBarSliderColor
    ctx.strokeStyle = style.scrollBarBorderColor
    ctx.lineWidth = 1
    const r = 3
    const g0 = 2
    const sBox = this.sliderBox
    strokeRoundRect(ctx, g0, g0, this.width - g0 * 2 - 1, this.height - g0 * 2 - 1, r)
    if (this.len === 0.0) {
      return
    }
    fillRoundRect(ctx, sBox.x, sBox.y, sBox.w, sBox.h, r - 1)
  }

  private updateSlierBox() {
    if (this.horizon) {
      this.updateSlierBoxHorizon()
    } else {
      this.updateSlierBoxVertical()
    }
  }

  private updateSlierBoxHorizon() {
    const g1 = 4
    const w = this.width - g1 * 2
    const h = this.height - g1 * 2
    const bx = this.sliderBox
    bx.x = g1 + w * this.pos
    bx.y = g1
    bx.w = w * this.len
    bx.h = h
    if (this.len > 0) {
      if (bx.w < 8) {
        bx.w = 8
      }
      if (bx.x > w - 8 + g1) {
        bx.x = w - 8 + g1
      }
    }
  }

  private updateSlierBoxVertical() {
    const g1 = 4
    const w = this.width - g1 * 2
    const h = this.height - g1 * 2
    const bx = this.sliderBox
    bx.w = w
    bx.h = h * this.len
    bx.x = g1
    bx.y = g1 + h - h * (this.pos + this.len)
    if (this.len > 0) {
      if (bx.h < 8) {
        bx.h = 8
      }
      if (bx.y > h - 8 + g1) {
        bx.y = h - 8 + g1
      }
    }
  }
}

interface TimeData {
  t0: number
  t1: number
  pageTime: number
  minTime: number
  maxTime: number
}

export class TimeLineChart {
  private root: HTMLDivElement
  private canvas: HTMLCanvasElement
  private w = 0
  private h = 0
  private timeLineList = new TimeLineList()
  // private timeLines: TimeLine[] = []
  // private contentBox: Rect = { x: 0, y: 0, w: 0, h: 0 }
  private timeData: TimeData = {
    t0: 0,
    t1: 0,
    pageTime: 0,
    minTime: 0,
    maxTime: 24 * 3600
  }

  private timeScrollBar = new ScrollBar(false)
  private hScrollBar = new ScrollBar(true)

  private handleMouseWheel = this.onMouseWheel.bind(this)
  private handleMouseClick = this.onMouseClick.bind(this)

  constructor(div: HTMLDivElement) {
    this.root = div
    this.canvas = this.initCanvas(div)
    this.initGeometry()
    this.initTimeData()
    this.canvas.addEventListener('wheel', this.handleMouseWheel)
    this.canvas.addEventListener('mousedown', this.handleMouseClick)
    this.updateData([
      {
        title: '',
        left: [],
        right: []
      }
    ])
  }

  public clearData() {
    this.updateData([
      {
        title: '',
        left: [],
        right: []
      }
    ])
  }

  public updateData(lst: TimeLineSeries[]) {
    this.setData(lst)
    this.draw()
  }

  public destroy() {
    this.canvas.removeEventListener('wheel', this.handleMouseWheel)
    this.canvas.removeEventListener('mousedown', this.handleMouseClick)
    this.root.removeChild(this.canvas)
  }

  private onMouseWheel(e: Event) {
    const we = e as WheelEvent
    this.setT0(this.timeData.t0 - 1 * we.deltaY)
    this.draw()
  }

  private draw() {
    const ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
    this.timeLineList.draw(ctx)
    this.timeScrollBar.draw(ctx)
    this.hScrollBar.draw(ctx)
  }

  private initTimeData() {
    const pageTime = (this.timeLineList.height / dimension.timeScaleHeightPerMinute) * 60
    this.timeData = {
      t0: 0,
      t1: pageTime,
      pageTime,
      minTime: 0,
      maxTime: 3600 * 48
    }
    this.setT0(0)
  }

  private initCanvas(root: HTMLDivElement) {
    const canvas = document.createElement('canvas')
    root.appendChild(canvas)
    const w = root.clientWidth
    const h = root.clientHeight
    canvas.style.width = w + 'px'
    canvas.style.height = h + 'px'
    canvas.width = w
    canvas.height = h
    return canvas
  }

  private initGeometry() {
    this.w = this.canvas.width
    this.h = this.canvas.height
    this.timeScrollBar.setGeometry(0, 0, dimension.scrollBarWidth, this.h)
    // this.hScrollBar.setGeometry(Dimension.scrollBarWidth, this.h - Dimension.scrollBarWidth, this.w - Dimension.scrollBarWidth, Dimension.scrollBarWidth)
    // this.timeLineList.setGeometry(
    //   Dimension.scrollBarWidth,
    //   0,
    //   this.w - Dimension.scrollBarWidth,
    //   this.h - Dimension.scrollBarWidth
    // )
    this.timeLineList.setGeometry(dimension.scrollBarWidth, 0, this.w, this.h)
  }

  private setT0(t: number) {
    const td = this.timeData
    if (t < td.minTime) {
      t = td.minTime
    }
    if (t > td.maxTime - td.pageTime) {
      t = td.maxTime - td.pageTime
    }
    td.t0 = t
    td.t1 = t + td.pageTime
    this.timeLineList.setTimeRange(td.t0, td.t1)
    this.timeScrollBar.setSliderGeometryX(td.minTime, td.maxTime, td.t0, td.t1)
  }

  private setData(lst: TimeLineSeries[]) {
    for (const e of lst) {
      e.left.sort((a, b) => a.time - b.time)
      e.right.sort((a, b) => a.time - b.time)
    }
    this.timeLineList.setData(lst)
    this.setTimeData(lst)
    this.setHPos(0)
    this.updateHScrollBar()
  }

  private setTimeData(lst: TimeLineSeries[]) {
    let min = 0
    let max = 3600 * 24 - 300
    lst.forEach((e) => {
      e.left.forEach((a) => {
        const t = a.time
        if (t < min) {
          min = t
        }
        if (t > max) {
          max = t
        }
      })
      e.right.forEach((a) => {
        const t = a.time
        if (t < min) {
          min = t
        }
        if (t > max) {
          max = t
        }
      })
    })
    if (min > 300) {
      min -= 300
    }
    max += 300
    if (max - min < this.timeData.pageTime) {
      max = min + this.timeData.pageTime
    }
    this.timeData.minTime = min
    this.timeData.maxTime = max
    this.setT0(min)
  }

  private updateHScrollBar() {
    if (!this.timeLineList.scroll) {
      this.hScrollBar.visible = false
      this.timeLineList.setGeometry(dimension.scrollBarWidth, 0, this.w, this.h)
      return
    }
    this.hScrollBar.visible = true
    this.hScrollBar.setGeometry(
      dimension.scrollBarWidth,
      this.h - dimension.scrollBarWidth,
      this.w - dimension.scrollBarWidth,
      dimension.scrollBarWidth
    )
    this.timeLineList.setGeometry(
      dimension.scrollBarWidth,
      0,
      this.w - dimension.scrollBarWidth,
      this.h - dimension.scrollBarWidth
    )
  }

  private setHPos(h: number) {
    if (!this.timeLineList.scroll) {
      return
    }
    this.hScrollBar.visible = true
    this.timeLineList.setHPos(h)
    const box = this.timeLineList.contentBox
    this.hScrollBar.setSliderGeometryX(0, box.w, -box.x, -box.x + this.timeLineList.width)
  }

  private onMouseClick(e: Event) {
    const me = e as MouseEvent
    let ok = this.timeScrollBar.insideSlier(me.offsetX, me.offsetY)
    if (ok) {
      this.handleTimeSlider()
      return
    }
    ok = this.hScrollBar.insideSlier(me.offsetX, me.offsetY)
    if (ok) {
      this.handleHScrollSlider()
    }
  }

  private handleHScrollSlider() {
    const onMouseMove = (e: Event) => {
      const me = e as MouseEvent
      const dx = me.movementX
      this.setHPos(this.timeLineList.hPosition + dx)
      this.draw()
    }
    const onMouseUp = () => {
      document.removeEventListener('mousemove', onMouseMove)
      document.removeEventListener('mouseup', onMouseUp)
    }
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }

  private handleTimeSlider() {
    const p = this.timeData.maxTime - this.timeData.minTime
    const h = this.timeScrollBar.sliderRange
    const fc = p / h
    const onMouseMove = (e: Event) => {
      const me = e as MouseEvent
      const dy = me.movementY
      const dt = dy * fc
      const t = this.timeData.t0 - dt
      this.setT0(t)
      this.draw()
    }
    const onMouseUp = () => {
      document.removeEventListener('mousemove', onMouseMove)
      document.removeEventListener('mouseup', onMouseUp)
    }
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }
}
